/*****************************************************************************/
/* This is the program skeleton for homework 2 in CS 184 by Ravi Ramamoorthi */
/* Extends HW 1 to deal with shading, more transforms and multiple objects   */
/*****************************************************************************/


#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <deque>
#include <stack>
#include <GL/glew.h>
#include <GL/glut.h>
#include "shaders.h"
#include "Transform.h"
#include <FreeImage.h>


using namespace std ; 

// Main variables in the program.  
#define MAINPROGRAM 
#include "variables.h" 
#include "ObjFileReader.h"
#include "AnimatedGate.h"
#include "readfile.h" // prototypes for readfile.cpp  
void display(void) ;  // prototype for display function.  

// Uses the Projection matrices (technically deprecated) to set perspective 
// We could also do this in a more modern fashion with glm.  
void reshape(int width, int height){
	w = width;
	h = height;
        mat4 mv ; // just like for lookat

	glMatrixMode(GL_PROJECTION);
        float aspect = w / (float) h, zNear = 0.1, zFar = 99.0 ;
        // I am changing the projection stuff to be consistent with lookat
        if (useGlu) mv = glm::perspective(fovy,aspect,zNear,zFar) ; 
        else {
          mv = Transform::perspective(fovy,aspect,zNear,zFar) ; 
          mv = glm::transpose(mv) ; // accounting for row major 
        }
        glLoadMatrixf(&mv[0][0]) ; 

	glViewport(0, 0, w, h);
}

void saveScreenshot(string fname) {
	int pix = w * h;
	BYTE *pixels = new BYTE[3*pix];	
	glReadBuffer(GL_FRONT);
	glReadPixels(0,0,w,h,GL_BGR,GL_UNSIGNED_BYTE, pixels);

	FIBITMAP *img = FreeImage_ConvertFromRawBits(pixels, w, h, w * 3, 24, 0xFF0000, 0x00FF00, 0x0000FF, false);
	
	std::cout << "Saving screenshot: " << fname << "\n";

	FreeImage_Save(FIF_PNG, img, fname.c_str(), 0);
	delete pixels;
}


void printHelp() {
  std::cout << "\npress 'h' to print this message again.\n" 
       << "press '+' or '-' to change the amount of rotation that\noccurs with each arrow press.\n" 
            << "press 'g' to switch between using glm::lookAt and glm::Perspective or your own LookAt.\n"       
            << "press 'r' to reset the transformations.\n"
            << "press 'm' to enter first-person movement mode.\n"
            << "press 'z' to toggle textures\n"
            << "press 'c' to toggle cel shading\n"
            << "press 'b' to toggle black and white rendering\n"
            << "press 'x' to open/close the gate\n"
			<< "press '0' to toggle bump mapping\n"
            << "press 'd' to print debugging info\n"
            << "press 'v' 't' 's' to do view [default], translate, scale.\n"
            << "press ESC to quit.\n" ;      

}


void keyboard(unsigned char key, int x, int y) {
	switch(key) {
	case '+':
		amount++;
		std::cout << "amount set to " << amount << "\n" ;
		break;
	case '-':
		amount--;
		std::cout << "amount set to " << amount << "\n" ; 
		break;
	case 'g':
		useGlu = !useGlu;
                reshape(w,h) ; 
		std::cout << "Using glm::LookAt and glm::Perspective set to: " << (useGlu ? " true " : " false ") << "\n" ; 
		break;
	case 'h':
		printHelp();
		break;
        case 27:  // Escape to quit
                exit(0) ;
                break ;
        case 'r': // reset eye and up vectors, scale and translate. 
		eye = eyeinit ; 
		up = upinit ; 
		center = vec3(0,0,0);
                sx = sy = 1.0 ; 
                tx = ty = 0.0 ; 
		break ;   
        case 'v': 
                transop = view ;
                std::cout << "Operation is set to View\n" ; 
                break ; 
        case 't':
                transop = translate ; 
                std::cout << "Operation is set to Translate\n" ; 
                break ; 
        case 's':
                transop = scale ; 
                std::cout << "Operation is set to Scale\n" ; 
                break ; 
        case 'm':
                transop = firstp ; 
                std::cout << "Operation is set to First Person Mode\n" ; 
                break ; 
        case 'd':
                std::cout << "Debugging Info:\n" ; 
				printf("Eye position: %.3f,%.3f,%.3f\n", eye[0],eye[1],eye[2]);
				printf("Center position: %.3f,%.3f,%.3f\n", center[0],center[1],center[2]);
				printf("Up vector: %.3f,%.3f,%.3f\n", up[0],up[1],up[2]);
				printf("Look direction: %.3f,%.3f,%.3f\n", (center-eye)[0],(center-eye)[1],(center-eye)[2]);
                break ; 
		case 'z':
				if( useTextures){
					glUniform1i( enable_textures, 0);
					useTextures = false;
					std::cout << "Textures off \n";
				}else{
					glUniform1i( enable_textures, 1);
					useTextures = true;
					std::cout << "Textures on \n";
				}
				break;
		case 'x':
			AnimatedGate::toggleState(doorstate);
			break;
		case 'c':
			if( useCel){
			glUniform1i( enable_cel_shading, 0);//disable cel-shading
			useCel = false;
			std::cout << "Cel shading off \n";
			}else{
			glUniform1i( enable_cel_shading, 1);//enable cel-shading
			useCel = true;
			std::cout << "Cel shading on \n";
			}
			break;
		case 'b':
			if( useBW){
			glUniform1i( enable_bw, 0);//disable cel-shading
			useBW = false;
			std::cout << "Black and white rendering off \n";
			}else{
			glUniform1i( enable_bw, 1);//enable cel-shading
			useBW = true;
			std::cout << "Black and white rendering on \n";
			}
			break;
		case '0':
				useBump = !useBump;
				if( useBump){
					glUniform1i( enable_bump, 1);
					//useBump= false;
					std::cout << "Bump map on \n";
				}else{
					glUniform1i( enable_bump, 0);
					//useBump = true;
					std::cout << "Bump map off \n";
				}
				break;
        }
	glutPostRedisplay();
}

//  You will need to enter code for the arrow keys 
//  When an arrow key is pressed, it will call your transform functions
float fspeed = .015;
float bspeed = .0075;
float sspeed = .01;
void specialKey(int key, int x, int y) {
	switch(key) {
	case 100: //left
          if (transop == view) Transform::left(amount, eye,  up);
          else if (transop == scale) sx -= amount * sspeed; 
          else if (transop == translate) tx -= amount * sspeed ; 
		  else if (transop == firstp){
				//strafe left
				vec3 side = glm::normalize(glm::cross( up, eye-center));
				Transform::scale(side, amount*sspeed);
				side[2] = 0; //zero out the z-movement
				eye -= side;
				center -= side;
		  }
          break;
	case 101: //up
          if (transop == view) Transform::up(amount,  eye,  up);
          else if (transop == scale) sy += amount * fspeed ; 
          else if (transop == translate) ty += amount * fspeed ; 
		  else if (transop == firstp){
				  //move forward
				 vec3 eyedir = (glm::normalize(eye-center));
				 Transform::scale(eyedir, amount*fspeed);
				 eyedir[2] = 0; //zero out the z-movement
				 eye -= eyedir;
				 center -= eyedir;
		  }
          break;
	case 102: //right
          if (transop == view) Transform::left(-amount, eye,  up);
          else if (transop == scale) sx += amount * sspeed ; 
          else if (transop == translate) tx += amount * sspeed ; 
		  else if (transop == firstp){
				//strafe right
				vec3 side = glm::normalize(glm::cross( up, eye-center));
				Transform::scale(side, amount*sspeed);
				side[2] = 0; //zero out the z-movement
				eye += side;
				center += side;
		  }
          break;
	case 103: //down
          if (transop == view) Transform::up(-amount,  eye,  up);
          else if (transop == scale) sy -= amount * bspeed ; 
          else if (transop == translate) ty -= amount * bspeed ; 
		  else if (transop == firstp){
				//move back
				 vec3 eyedir = (glm::normalize(eye-center));
				 Transform::scale(eyedir, amount*bspeed);
				 eyedir[2] = 0; //zero out the z-movement
				 eye += eyedir;
				center += eyedir;
		  }
          break;
	}
	glutPostRedisplay();
}

void pmotion( int x, int y){
	if( transop == firstp){
		if( !mouseInit){ //if mouse has not ben initialized yet
			mouseX = x; mouseY = y; mouseInit = true;	
			return;
		}
		
		//printf("x: %d/%d, y:%d/%d\n", x,w,y,h);
		//mouseX and mouseY hold previous values
		float xdiff = float(x - mouseX) / w;
		float ydiff = float(y - mouseY) / h;
		
		vec3 xoffset=vec3(0), yoffset=vec3(0);
		mat3 rotmat;
		vec3 eyedir = center-eye;
		//left-right
		rotmat = Transform::rotate( xdiff*(amount*200) , glm::normalize(up));
		xoffset = glm::normalize(rotmat*eyedir);
		//printf("xoffset x %f, y %f z %f\n",xoffset[0], xoffset[1], xoffset[2]);
		//printf("eyedir x %f, y %f z %f\n\n",eyedir[0], eyedir[1], eyedir[2]);

		//up-down
		vec3 side = glm::cross(center-eye, up);
		rotmat = Transform::rotate( ydiff*(amount*200) , glm::normalize(side));
		yoffset = glm::normalize(rotmat*eyedir);

		center = eye + xoffset + yoffset;





		//update previous values
		mouseX = x;
		mouseY = y;
		//redraw the screen
		glutPostRedisplay();
	}
}

void init() {
      // Initialize shaders
      vertexshader = initshaders(GL_VERTEX_SHADER, "shaders/light.vert.glsl") ;
      fragmentshader = initshaders(GL_FRAGMENT_SHADER, "shaders/light.frag.glsl") ;
      shaderprogram = initprogram(vertexshader, fragmentshader) ; 
      enablelighting = glGetUniformLocation(shaderprogram,"enablelighting") ;
      lightpos = glGetUniformLocation(shaderprogram,"lightposn") ;       
      lightcol = glGetUniformLocation(shaderprogram,"lightcolor") ;       
      numusedcol = glGetUniformLocation(shaderprogram,"numused") ;       
      ambientcol = glGetUniformLocation(shaderprogram,"ambient") ;       
      diffusecol = glGetUniformLocation(shaderprogram,"diffuse") ;       
      specularcol = glGetUniformLocation(shaderprogram,"specular") ;       
      emissioncol = glGetUniformLocation(shaderprogram,"emission") ;       
      shininesscol = glGetUniformLocation(shaderprogram,"shininess") ;       

      texture_location = glGetUniformLocation(shaderprogram,"tex") ;       
      textured_object = glGetUniformLocation(shaderprogram,"textured_object") ;     
	  bumpmap_location = glGetUniformLocation(shaderprogram,"bumpmap") ; 
	  bumpmap_object = glGetUniformLocation(shaderprogram,"bumpmap_object") ;     

      enable_textures = glGetUniformLocation(shaderprogram,"enable_textures") ;
      enable_cel_shading = glGetUniformLocation(shaderprogram,"enable_cel_shading") ;
      enable_bw = glGetUniformLocation(shaderprogram,"enable_bw") ;
      enable_bump = glGetUniformLocation(shaderprogram,"enable_bump") ;

	  glUniform1i( enable_textures, 1); //enable textures by default
	  glUniform1i( enable_cel_shading, 0); //disable cel shading by default
	  glUniform1i( enable_bw, 0); //disable cel shading by default
	  glUniform1i( enable_bump, 1); //enable textures by default


	  useTextures = true;
	  useCel = false;
	  useBW = false;
	  useBump=true;
	  animate = 0;
	  doorstate = CLOSED;

	  //for multiple instancing
	  float stardist = 20;
	  for(int i=0;i<numstars;i++){
		  stars[i] = vec3(rand()%1000-500,rand()%1000-500,abs(rand()%500));
		  stars[i] = stardist*glm::normalize(stars[i]) + vec3(0,0,-1);
	  }

	glGenBuffers(numperobj*numobjects+ncolors+1, buffers) ; 
	initcolorscube() ; 
	initobjectnocol(CUBE, (GLfloat *) cubeverts, sizeof(cubeverts), (GLubyte *) cubeinds, sizeof (cubeinds), GL_QUADS) ; 
}


void idle(){
	  animate++;
	  if( animate>10000){
		animate = 0;
	  }
	  glutPostRedisplay();
}

int main(int argc, char* argv[]) {
	if (argc < 2) {
		cerr << "Usage: transforms scenefile [grader input (optional)]\n"; 
		exit(-1); 
	}

  	FreeImage_Initialise();
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
	glutCreateWindow("HW4");

	GLenum err = glewInit() ; 
	if (GLEW_OK != err) { 
		std::cerr << "Error: " << glewGetString(err) << std::endl; 
	} 

	init();
    readfile(argv[1]) ; 

	glutDisplayFunc(display);
	glutSpecialFunc(specialKey);
	glutKeyboardFunc(keyboard);
	mouseInit = false;
	glutPassiveMotionFunc(pmotion);
	glutReshapeFunc(reshape);
	glutReshapeWindow(w, h);
	glutIdleFunc(idle);

	printHelp();
	glutMainLoop();
	FreeImage_DeInitialise();
	return 0;
}
